﻿
using OnlineMealOrdering.Domain.Base;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Reflection;
using System.Text;

namespace De.EP.Persistence
{
    public class UniqueKeyBuilder
    {
        static IDictionary<string, string> _uniqueKeyErrorMsgs = new Dictionary<string, string>();

        public static IDictionary<string, string> UniqueKeyErrorMsgs
        {
            get { return _uniqueKeyErrorMsgs; }
        }

        DbContext _dbContext;

        public UniqueKeyBuilder(DbContext dbContext)
        {
            _dbContext = dbContext;
        }

        #region Services

        /// <summary>
        /// Handle attributions on entity
        /// Unique key will be generated here to database
        /// </summary>
        public void BuildUniqueKeys()
        {
            if (_dbContext != null)
            {
                List<PropertyInfo> dbSets = GetAllDbSetsOfDbContext(_dbContext.GetType());
                dbSets.ForEach(a =>
                {
                    var entityTypes = a.PropertyType.GenericTypeArguments;
                    if (entityTypes != null && entityTypes.Length > 0)
                    {
                        //DbSet<Entity> only has one entity type
                        var entityType = entityTypes[0];

                        //get all composite unique attributes and handle these attributes
                        HandleCompositeUniqueKeyOfDbSet(entityType);

                        //get all properties of dbset and handle their unique attributes
                        HandleUniqueKeyOfProperty(entityType);
                    }
                });
            }
        }

        /// <summary>
        /// Iterate all unique attributes of dbcontext and generate associated exception message when violate unique key constraint
        /// </summary>
        /// <param name="dbContextType"></param>
        public static void RegisterExceptionMessages(Type dbContextType)
        {
            List<PropertyInfo> dbSets = GetAllDbSetsOfDbContext(dbContextType);
            dbSets.ForEach(a =>
            {
                var entityType = a.PropertyType.GenericTypeArguments[0];
                List<UniqueKeyAttribute> entityUniqueKeys = GetCompositeUniqueKeysOfEntity(entityType);
                List<UniqueKeyAttribute> propertyUniqueKeys = GetUniqueKeyOfProperty(entityType);

                FillExceptionMsgs(entityUniqueKeys, entityType);
                FillExceptionMsgs(propertyUniqueKeys, entityType);
            });
        }

        #endregion

        #region Methods

        /// <summary>
        /// Get all dbsets which are defined in dbcontext
        /// </summary>
        /// <param name="dbContextType"></param>
        /// <returns></returns>
        static List<PropertyInfo> GetAllDbSetsOfDbContext(Type dbContextType)
        {
            PropertyInfo[] dbContextProps = dbContextType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            return dbContextProps.Where(a => a.PropertyType.Name == "DbSet`1" || a.PropertyType.Name == "IDbSet`1").ToList();
        }

        static void FillExceptionMsgs(List<UniqueKeyAttribute> uniqueKeys, Type entityType)
        {
            if (uniqueKeys != null && uniqueKeys.Count > 0)
            {
                uniqueKeys.ForEach(a =>
                {
                    string tableName = entityType.Name;
                    string[] arrFieldNames = a.UniqueKeyNames.Split(new char[] { ',' });
                    string ixName = "IX_" + tableName + "_" + string.Join("_", arrFieldNames);

                    if (!_uniqueKeyErrorMsgs.ContainsKey(ixName))
                        _uniqueKeyErrorMsgs.Add(ixName, a.ErrorMessage);
                });
            }
        }

        /// <summary>
        /// Get all single unique keys of entity's property
        /// Normally, one property has one unique key
        /// </summary>
        /// <param name="entityType"></param>
        /// <returns></returns>
        static List<UniqueKeyAttribute> GetUniqueKeyOfProperty(Type entityType)
        {
            List<UniqueKeyAttribute> attributes = new List<UniqueKeyAttribute>();

            var entityProps = entityType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            entityProps.ToList().ForEach(b =>
            {
                var uniqueAttrOfProp = b.GetCustomAttribute(typeof(UniqueKeyAttribute)) as UniqueKeyAttribute;
                if (uniqueAttrOfProp != null)
                {
                    //Because unique attribute of property do not set property "UniquekeyNames", 
                    //so we set "UniqueKeyNames" as property's type name here.
                    uniqueAttrOfProp.UniqueKeyNames = b.Name;
                    attributes.Add(uniqueAttrOfProp);
                }
            });

            return attributes;
        }

        /// <summary>
        /// Get all composite unique keys of entity
        /// One entity may has multiple composite unique key
        /// </summary>
        /// <param name="entityType"></param>
        /// <returns></returns>
        static List<UniqueKeyAttribute> GetCompositeUniqueKeysOfEntity(Type entityType)
        {
            //If UniqueKey marked on the class, indicate it is a composite unique key.

            List<UniqueKeyAttribute> attributes = new List<UniqueKeyAttribute>();

            var arrUniqueKey = entityType.GetCustomAttributes(typeof(UniqueKeyAttribute), false) as UniqueKeyAttribute[];
            if (arrUniqueKey != null && arrUniqueKey.Length > 0)
                attributes.AddRange(arrUniqueKey);

            return attributes;
        }

        /// <summary>
        /// Generate unique key with single field in table
        /// </summary>
        /// <param name="entityType"></param>
        private void HandleUniqueKeyOfProperty(Type entityType)
        {
            List<UniqueKeyAttribute> uniqueKeys = GetUniqueKeyOfProperty(entityType);
            if (uniqueKeys != null)
                uniqueKeys.ForEach(a =>
                {
                    string ixName;
                    var script = CreateUniqueKeyScript(entityType, a, out ixName);
                    ExecuteUniqueKeyScript(script, ixName);
                });
        }

        internal static string CreateUniqueKeyScript(Type entityType, UniqueKeyAttribute uniqueKeyAtt, out string ixName)
        {
            string tableName;
            string fieldName;
            ParseUniqueKey(entityType, uniqueKeyAtt, out ixName, out tableName, out fieldName);

            return GenerateAddUniqueConstraint(tableName, fieldName, ixName);
        }

        internal static string DropUniqueKeyScript(Type entityType, UniqueKeyAttribute uniqueKeyAtt, out string ixName)
        {
            string tableName;
            string fieldName;
            ParseUniqueKey(entityType, uniqueKeyAtt, out ixName, out tableName, out fieldName);

            return GenerateDropUniqueConstraint(tableName, ixName);
        }

        private static void ParseUniqueKey(Type entityType, UniqueKeyAttribute uniqueKeyAtt, out string ixName, out string tableName, out string fieldName)
        {
            tableName = entityType.Name;
            fieldName = string.Format("[{0}] ASC", uniqueKeyAtt.UniqueKeyNames);

            if (IsEntityProperty(entityType, uniqueKeyAtt.UniqueKeyNames))
                fieldName += "_Id";

            ixName = "IX_" + tableName + "_" + uniqueKeyAtt.UniqueKeyNames;
        }

        private static bool IsEntityProperty(Type entityType, string propertyName)
        {
            var pi = entityType.GetProperty(propertyName);
            if (pi == null)
                throw new ApplicationException(string.Format("Type {0} does not exist the property name {1}.", entityType.FullName, propertyName));

            if (pi.PropertyType.IsClass && pi.PropertyType != typeof(string))
                return true;

            return false;
        }

        /// <summary>
        /// Generate compose unique key with multi fields in table
        /// </summary>
        /// <param name="entityType"></param>
        private void HandleCompositeUniqueKeyOfDbSet(Type entityType)
        {
            List<UniqueKeyAttribute> compositeUniqueKeys = GetCompositeUniqueKeysOfEntity(entityType);
            compositeUniqueKeys.ForEach(a =>
            {
                string ixName;
                var script = CreateCompositeUniqueKeyScript(entityType, a, out ixName);
                ExecuteUniqueKeyScript(script, ixName);
            });
        }

        internal static string CreateCompositeUniqueKeyScript(Type entityType, UniqueKeyAttribute compositeUniqueKeyAtt, out string ixName)
        {
            string tableName;
            string fieldName;
            ParseCompositeUniqueKey(entityType, compositeUniqueKeyAtt, out ixName, out tableName, out fieldName);

            return GenerateAddUniqueConstraint(tableName, fieldName, ixName);
        }

        internal static string DropCompositeUniqueKeyScript(Type entityType, UniqueKeyAttribute compositeUniqueKeyAtt, out string ixName)
        {
            string tableName;
            string fieldName;
            ParseCompositeUniqueKey(entityType, compositeUniqueKeyAtt, out ixName, out tableName, out fieldName);

            return GenerateDropUniqueConstraint(tableName, ixName);
        }

        private static void ParseCompositeUniqueKey(Type entityType, UniqueKeyAttribute compositeUniqueKeyAtt, out string ixName, out string tableName, out string fieldName)
        {
            tableName = entityType.Name;
            string[] arrFieldNames = compositeUniqueKeyAtt.UniqueKeyNames.Split(new char[] { ',' });
            ixName = "IX_" + tableName + "_" + string.Join("_", arrFieldNames);
            fieldName = string.Empty;
            foreach (string field in arrFieldNames)
            {
                if (IsEntityProperty(entityType, field))
                    fieldName += "[" + field + "_Id] ASC,";
                else
                    fieldName += "[" + field + "] ASC,";
            }

            fieldName = fieldName.TrimEnd(',');
        }

        /// <summary>
        /// According to result of resolving unique key attribute to generate sql script and execute it on data base
        /// </summary>
        /// <param name="tableName"></param>
        /// <param name="fieldName"></param>
        /// <param name="ixName"></param>
        private void ExecuteUniqueKeyScript(string sqlScript, string ixName)
        {
            try
            {
                _dbContext.Database.ExecuteSqlCommand(sqlScript);
            }
            catch (Exception)
            {
                throw new EPSystemException("创建唯一索引{0}失败", ixName);
            }
        }

        private static string GenerateAddUniqueConstraint(string tableName, string fieldName, string ixName)
        {
            string sqlScript = string.Format("ALTER TABLE [{0}] WITH NOCHECK ADD CONSTRAINT [{1}] UNIQUE NONCLUSTERED ({2})",
                tableName,
                ixName,
                fieldName
                );
            return sqlScript;
        }

        private static string GenerateDropUniqueConstraint(string tableName, string ixName)
        {
            string sqlScript = string.Format("ALTER TABLE [{0}] DROP CONSTRAINT [{1}]",
                tableName,
                ixName
                );
            return sqlScript;
        }

        #endregion
    }
}
